iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0

Hi,大家好,昨天我們加入了資料驗證機制的設定後,輸入表單已經是有了初步的雛形了,今天我們要來加強表單的功能。多數在做問題反應的機制上,一定會加上檔案上傳的功能,以方便實際上處理問題的人有參考資料,今天我們就來把圖片上傳功能加進去吧

上傳的方式

在html中,要做到檔案上傳時,100%會用到的東西是input中的 file 物件,但是上傳的做法就有數種了,第一種是最傳統的,以form post 來做上傳動作

<form action="upload.php" method="post" enctype="multipart/form-data">
  Select image to upload:
  <input type="file" name="fileToUpload" id="fileToUpload">
  <input type="submit" value="Upload Image" name="submit">
</form>

說明:這是最傳統的做法,準備表單並指定method="post" 與 enctype="multipart/form-data" ,表單送出後就完成上傳動作

第二種是透過 ajax 進行上傳

  const app = new Vue({
    data: () => ({images: null}),
    template: `
      <div>
        <input type="file" @change="uploadFile" ref="file">
        <button @click="submitFile">Upload!</button>
      </div>
    `,
    methods: {
      uploadFile() {
        this.Images = this.$refs.file.files[0];
      },
      submitFile() {
        const formData = new FormData();
        formData.append('file', this.Images);
        const headers = { 'Content-Type': 'multipart/form-data' };
        axios.post('https://httpbin.org/post', formData, { headers }).then((res) => {
          res.data.files; // binary representation of the file
          res.status; // HTTP status
        });
      }
    }
  });
  app.$mount("#content");

說明:第二種做法其實是第一種做法的 vue.js 版本,將資料轉換成為 FormData ,並在表單中加入 { 'Content-Type': 'multipart/form-data' } 的 header 後透過 axios 送出。
上述2種方法都是可用的,也是常見的做法。不過今天我們來點不一樣的,透過 fileReader 將檔案轉成 dataURL 後,把檔案當成普通文字資料上傳,那麼我們開始吧

什麼是 dataURL?可以吃嗎?

dataURL 可以理解成為是一個資料傳輸的格式,是由WHATWG 制訂出來的,它主要的資料結構是「data:[][;base64], 」,例如: data:text/plain;base64,SGVsbG8sIFdvcmxkIQ== ,它最方便的地方就是因為已經被轉換成 BASE64編碼資料了,所以傳輸的方式很簡單,當一般文字傳輸就行了。另外還有一個方便的地方是如果資料內容是圖檔的話,將 dataURL 的內文指定給 img 的 src 屬性,瀏覽器會自動把圖檔打開來顯示,可以做到前端預覽圖檔的效果。
那麼,今天的程式碼如下

<style>
    .formtitle::before {
        color: red;
        content: "*";
    }
    .err {
        color: red;
        background-color: pink;
    }
</style>
<div id="app">
    <div class="d-flex align-items-center py-4 bg-body-tertiary">
        <main class="form-signin w-100 m-auto">
            <h1 class="h3 mb-3 fw-normal">請輸入您的問題</h1>

            <div class="form-floating">
                <input type="text" v-model="data.casedesc" :class="{ err: data.inErr }" class="form-control" id="floatingInput">
                <label for="floatingInput"  class="formtitle" :class="{ err: data.inErr }" > 問題概述</label>
            </div>
            <label for="floatingPassword" class="formtitle" > 詳細說明</label>
            <div class="form-floating">
                <textarea v-model="data.caseinfo" id="floatingPassword" style="width: 100%;" rows="20"  :class="{ err: data.inErr }">
                </textarea>
            </div>
            <div class="form-floating">
                <input type="file" class="form-control" id="fileInput" accept="image/*" @change="readfile">
                <label for="fileInput">附件上傳</label>
                <img v-bind:src="data.fileinfo"/>
            </div>
            <button class="btn btn-primary w-100 py-2" @click="senddata()">送出</button>
        </main>
    </div>
</div>
<script>
    const { createApp, onBeforeMount, reactive } = Vue
    let data = reactive({ casedesc: "", caseinfo: "",  inErr: false})
    createApp({
        setup() {
            
            return { data }
        },
        methods: {
            readfile(e) {
                let file = e.target.files[0]
                let reader = new FileReader()
                if(file) {
                    reader.onload = function(e) {
                        data.fileinfo = reader.result;
                    }
                    reader.readAsDataURL(file)
                }
            },
            senddata() {
                if(data.casedesc === "" || data.caseinfo === "") {
                    alert("請務必輸入問題描述與說明")
                    data.inErr = true
                    return false
                }
                axios.post("/saf/mgr/api/saveCase", {
                    casedesc: data.casedesc,
                    caseinfo: data.caseinfo,
                    fileinfo: data.fileinfo
                }).then(function (response) {
                    let rt = response.data
                    if (rt.status == "00") {
                        alert("存檔成功")
                    }
                }).catch(function (response) {
                    alert(response.response.data)
                })
            }
        }
    })
        .mount('#app')
</script>

說明:

  1. 我們在 file物件上,加入 onChange 事件去呼叫「readfile」方法,該方法會宣告出一個 FileReader 物件,並執行 readAsDataURL ,將檔案讀取成為 dataURL 後,存放於 data.fileinfo 中
  2. 可以在 file 物件下方,看到 「」,有了這一行時,當 vue.js 讀取檔案,並存放於 data.fileinfo 中時,因為已經設好 bind 了,這個 img 會立刻顯示圖檔出來
  3. 資料送出時則非常的單純,當文字資料直接送出就可以了

將 dataURL 轉成檔案

將 dataURL 轉成檔案也是很簡單,我們只要用簡單的文字處理方式就可以做到

router.post("/api/saveCase", async (req, res, next) => {
    //讀取表單內容
    let fileinfo = req.body.fileinfo
    let regex = /^data:.+\/(.+);base64,(.*)$/;
    //以 re 過慮文字後,轉為資料陣列,其中陣列的最後一項即為以 BASE64 編碼的檔案資料
    let matches = fileinfo.match(regex);
    let ext = matches[1];
    let data = matches[2];
    //BASE64 轉為 buffer 後寫入檔案即可
    let buffer = Buffer.from(data, 'base64');
    fs.writeFileSync(path.join(__dirname, "file" + ext, buffer))
    //...
})

說明:簡單的說,就是把文字切開後,截取我們要的部份進行 base64 解碼後並存成檔案即可

結語

今天完成了圖檔上傳的功能,可以看出來,以dataURL 的檔案上傳其實就是這麼簡單,但是它有個缺點是不會夾帶檔案資訊上去,如同我存檔的程式碼所顯示的狀況一樣,系統只知道這是什麼檔案類型而已,若是存檔時需要保留原始檔名,或是需要過取得檔案的建立日期等資訊,dataURL 可能就不適用了。今天完成了檔案上傳的功能之後,我們明天要來進行圖檔檢視功能的製作,那麼,我們明天再繼續吧


上一篇
關於資料驗證的做法
下一篇
加入附件顯示功能
系列文
以vue.js + node.js 搭建一個客服填單系統30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言